package com.hyzs.service.impl;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.hyzs.service.FulltextService;
import com.hyzs.utils.EsConnUtils;
import com.hyzs.utils.LabelKeyWords;
import com.hyzs.utils.StringUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.lucene.search.TotalHits;
import org.elasticsearch.action.admin.indices.refresh.RefreshRequest;
import org.elasticsearch.action.admin.indices.refresh.RefreshResponse;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.delete.DeleteResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.search.SearchType;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.index.query.Operator;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.TermQueryBuilder;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.Stream;


@Service
@Qualifier("esService")
public class EsService implements FulltextService {

    private RestHighLevelClient client;

    private String judgementIndex = "judgement_v3";
    private final float threshold = 0.30f;

    @Autowired
    private EsConnUtils esConnUtils;

    private static final Logger logger = LoggerFactory.getLogger(EsService.class);

    // label max_score map:  <label: score>
    private static Map<String, Float> MAX_SCORE_MAP;

    public EsService() {
    }

    public EsService(RestHighLevelClient client) {
        this.client = client;
    }

    @PostConstruct
    public void initParameters(){
        MAX_SCORE_MAP = genMaxScoreMaps();
    }

    @Override
    public Map<String, Object> availableLabels() {
        return ImmutableMap.of("labels", ImmutableList.copyOf(LabelKeyWords.isAllLabels.keySet()));
    }

    @Override
    public Map<String, Object> queryContentLabels(String baseContent, String crimeName, int topK) {

        if(client == null) client = esConnUtils.getClient();
        final String randomId = RandomStringUtils.randomAlphabetic(12);
        addTempJudgementContent(client, randomId, baseContent);
        List<Map<String,Object>> resList = genLabelList(client, randomId, crimeName, topK);
        deleteJudgement(client, randomId);
        return ImmutableMap.of("data", resList);
    }


    @Override
    public Map<String, Object> queryContentLabelByCrimes(String baseContent, List<String> crimes) {
        String allLabelString = "all_labels";
        int topK = 10;

        if(client == null) client = esConnUtils.getClient();
        final String randomId = RandomStringUtils.randomAlphabetic(12);

        addTempJudgementContent(client, randomId, baseContent);
        List<Map<String,Object>> allList = genLabelList(client, randomId, allLabelString, topK);
        Map<String,Float> scoreMap = filterLabelList(allList);

        // check crime label_name validation
        crimes.forEach(crime -> Preconditions.checkArgument(
                MAX_SCORE_MAP.containsKey(crime),
                crime+ " invalid crime label name! "));

        List<Map<String,Object>> resList = crimes.stream()
                .map((String label) ->
                    ImmutableMap.of("label_name", label,
                            // hidden the score value, can uncomment this.
                            //"score", scoreMap.getOrDefault(label, 0.0f),
                            //"threshold", MAX_SCORE_MAP.getOrDefault(label, 0.0f) * threshold,
                            "value", (Object)scoreMap.containsKey(label)
                    )
                ).collect(Collectors.toList());
        deleteJudgement(client, randomId);
        return ImmutableMap.of("data", resList);
    }

    // labelList is sorted score list
    // filter label list by MAX_SCORE_MAP
    private Map<String,Float> filterLabelList(List<Map<String,Object>> labelList){
        //float maxScore = (Float) labelList.get(0).get("score");
        return labelList.stream()
                .filter(map ->
                        (Float)map.get("score") >=
                                MAX_SCORE_MAP.getOrDefault(map.get("label_name").toString(), 0.0f) * threshold
                )
                .collect(Collectors.toMap(
                        (Map<String,Object> map)-> map.get("label_name").toString(),
                        (Map<String,Object> map)-> (Float)map.get("score")
                ));
    }

    @Override
    public Map<String, Object> gJQueryContentLabels(String baseContent, List<String> crimeArray) {
        if(client == null) client = esConnUtils.getClient();
        final String randomId = RandomStringUtils.randomAlphabetic(12);
        addTempJudgementContent(client, randomId, baseContent);
        Map<String, Object> resMap = genQueryLabelCode(client,  randomId, crimeArray);
        deleteJudgement(client, randomId);
        return resMap;
    }


    List<Map<String,Object>> genLabelList(final RestHighLevelClient client,String doc_id, String L2Name, int topK){
        Preconditions.checkArgument(
                LabelKeyWords.L2Labels.contains(L2Name),
                "Not supported L2 query label!");
        Preconditions.checkArgument(
                topK>=1,
                "wrong value for return list length!");
        List<Map<String,Object>> resList = new ArrayList<>();
        switch (L2Name){
            case "WXJS_behavior":
                resList = generateLabelScore(client, doc_id, LabelKeyWords.isWXJS_behavior);
                break;
            case "JTZS_behavior":
                resList = generateLabelScore(client, doc_id, LabelKeyWords.isJTZS_behavior);
                break;
            case "ZSFMYSZZDP_circums":
                resList = generateLabelScore(client, doc_id, LabelKeyWords.isZSFMYSZZDP_circumstance);
                break;
            case "ZSFMYSZZDP_behavior":
                resList = generateLabelScore(client, doc_id, LabelKeyWords.isZSFMYSZZDP_behavior);
                break;
            case "XXZS_circums":
                resList = generateLabelScore(client, doc_id, LabelKeyWords.isXXZS_circumstance);
                break;
            case "XXZS_harm":
                resList = generateLabelScore(client, doc_id, LabelKeyWords.isXXZS_harm);
                break;
            case "XXZS_behavior":
                resList = generateLabelScore(client, doc_id, LabelKeyWords.isXXZS_behavior);
                break;
            case "penalty_heavier":
                resList = generateLabelScore(client, doc_id, LabelKeyWords.isMeasure_heavier);
                break;
            case "penalty_ease":
                resList = generateLabelScore(client, doc_id, LabelKeyWords.isMeasure_ease);
                break;
            case "penalty_ease_circumstance":
                resList = generateLabelScore(client, doc_id, LabelKeyWords.isMeasure_easeCir);
                break;
            case "all_labels":
                resList = generateLabelScore(client, doc_id, LabelKeyWords.isAllLabels);
                break;
            default:
                logger.error("bad format query label string!");
                break;
        }
        return topK <= resList.size() ? resList.subList(0,topK): resList;

    }

    // generate score list by label list
    // [{"label_name": "XXX", "score": "xxx"}, {...}, ...]
    List<Map<String,Object>> generateLabelScore(final RestHighLevelClient client,String doc_id, Map<String,String> labelMap){
        List<Map<String, Object>> scoreMapList = new ArrayList<>();
        labelMap.forEach((label, keyWord) -> {
            float score = compositeQueryScore(client, keyWord, doc_id);
            Map<String, Object> scoreMap = new HashMap<>();
            scoreMap.put("label_name", label);
            scoreMap.put("score", score);
            scoreMapList.add(scoreMap);
        });

//        AtomicInteger index = new AtomicInteger(0);
        List<Map<String, Object>> sortedList = scoreMapList.stream()
                .sorted(Comparator.comparing( (map -> (Float)map.get("score")), Comparator.reverseOrder()))
//                .map( map -> {
//                    map.put("rank", index.getAndIncrement());
//                    return map; })
                .collect(Collectors.toList());

        return sortedList;
    }

    // filter process is inefficient, use compositeQueryScore instead.
    @Deprecated
    float queryDocScore(final RestHighLevelClient client,String queryWord, String doc_id){
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.query(QueryBuilders.queryStringQuery(queryWord));
        sourceBuilder.size(EsConnUtils.MAX_SEARCH_LENGTH);
        sourceBuilder.timeout(new TimeValue(10, TimeUnit.SECONDS));
        SearchRequest searchRequest = new SearchRequest();
        searchRequest.indices(judgementIndex);
        searchRequest.source(sourceBuilder);
        searchRequest.searchType(SearchType.DFS_QUERY_THEN_FETCH);

        try {
            SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
            SearchHits hits = searchResponse.getHits();
            TotalHits totalHits = hits.getTotalHits();
            logger.info("keyword: {}, totalHits: {},  maxScore: {}", queryWord, totalHits, hits.getMaxScore());
            Optional<SearchHit> questHit = Stream.of(hits.getHits())
                    .filter(hit -> hit.getSourceAsMap().containsKey("temp"))
                    .filter(hit -> hit.getId().equals(doc_id))
                    .findFirst();
            if(questHit.isPresent()) return  questHit.get().getScore();
            else return 0;
        } catch (IOException e) {
            e.printStackTrace();
            return 0;
        }
    }

    //  calculate doc score by doc_id.
    //  SearchType.DFS_QUERY_THEN_FETCH to get constant scoring
    //  Normalizing query scores based on MAX score.
    float compositeQueryScore(final RestHighLevelClient client,String queryWord, String doc_id){
        QueryBuilder queryBuilder = QueryBuilders.boolQuery()
                .filter(QueryBuilders.termQuery("_id", doc_id))
                //.filter(QueryBuilders.termQuery("temp", true))
                .must(QueryBuilders.queryStringQuery(queryWord));

        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.query(queryBuilder);
        sourceBuilder.size(EsConnUtils.MAX_SEARCH_LENGTH);
        sourceBuilder.timeout(new TimeValue(10, TimeUnit.SECONDS));

        try {
            SearchResponse searchResponse = client.search(
                    new SearchRequest().indices(judgementIndex)
                            .searchType(SearchType.DFS_QUERY_THEN_FETCH)
                            .source(sourceBuilder),
                    RequestOptions.DEFAULT);
            SearchHits hits = searchResponse.getHits();
            //logger.info("keyword: {}", queryWord);
            //TotalHits totalHits = hits.getTotalHits();
            //logger.info("keyword: {}, totalHits: {},  maxScore: {}", queryWord, totalHits, hits.getMaxScore());
            Optional<SearchHit> questHit = Stream.of(hits.getHits())
                    //.filter(hit -> hit.getId().equals(doc_id))
                    .findFirst();
            if(questHit.isPresent()) return  questHit.get().getScore();
            else return 0;
        } catch (IOException e) {
            e.printStackTrace();
            return 0;
        }
    }

    float queryTermScore(final RestHighLevelClient client,String queryWord, String doc_id){
        QueryBuilder queryBuilder = QueryBuilders.boolQuery()
                .filter(QueryBuilders.termQuery("_id", doc_id))
                .must(QueryBuilders.matchQuery("content", queryWord).operator(Operator.AND));
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.query(queryBuilder);
        sourceBuilder.size(EsConnUtils.MAX_SEARCH_LENGTH);
        sourceBuilder.timeout(new TimeValue(10, TimeUnit.SECONDS));
        try {
            SearchResponse searchResponse = client.search(
                    new SearchRequest().indices(judgementIndex)
                            .searchType(SearchType.DFS_QUERY_THEN_FETCH)
                            .source(sourceBuilder),
                    RequestOptions.DEFAULT);
            SearchHits hits = searchResponse.getHits();
            logger.info("keyword: {}", queryWord);
            Optional<SearchHit> questHit = Stream.of(hits.getHits())
                    .findFirst();
            if(questHit.isPresent()) return  questHit.get().getScore();
            else return 0;
        } catch (IOException e) {
            e.printStackTrace();
            return 0;
        }
    }

    // TODO: label code
    Map<String, Object> genQueryLabelCode(final RestHighLevelClient client,String doc_id, List<String> crimeArray){

        Map<String, Object> resMap = new HashMap<>();
        LabelKeyWords.isAllLabels.forEach((label, keyWord) -> {

        });

        return resMap;
    }

    public void addTempJudgementContent(RestHighLevelClient client, String doc_id, String baseContent){
        Map<String, Object> jsonMap = new HashMap<>();
        jsonMap.put("content", StringUtils.trimJudgementContent(StringUtils.trimBlank(baseContent)));
        jsonMap.put("temp", true);
        IndexRequest indexRequest = new IndexRequest(judgementIndex)
                .id(doc_id).source(jsonMap);
        IndexResponse indexResponse = null;
        try {
            indexResponse = client.index(indexRequest, RequestOptions.DEFAULT);
            logger.info("Add temp {} index: {}", judgementIndex, doc_id);
            logger.info("Response {}", indexResponse);
            RefreshRequest refreshRequest = new RefreshRequest(judgementIndex);
            RefreshResponse refreshResponse = client.indices().refresh(refreshRequest, RequestOptions.DEFAULT);
            logger.info("refresh indices: {}", refreshResponse.getSuccessfulShards());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void deleteJudgement(RestHighLevelClient client, String doc_id){
        DeleteRequest deleteRequest = new DeleteRequest(judgementIndex, doc_id);
        try {
            DeleteResponse response = client.delete(deleteRequest, RequestOptions.DEFAULT);
            logger.info("delete temp {} index: {}",judgementIndex, doc_id);
            logger.info("Response {}", response);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    public Map<String, Float> genMaxScoreMaps(){
        if(client == null) client = esConnUtils.getClient();
        ImmutableMap.Builder<String, Float> resBuilder = ImmutableMap.builder();
        for(Map.Entry<String, String> entry: LabelKeyWords.isAllLabels.entrySet()){
            SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
            QueryBuilder builder = QueryBuilders.boolQuery()
                    .must(QueryBuilders.queryStringQuery(entry.getValue()));
            sourceBuilder.query(builder);
            sourceBuilder.timeout(new TimeValue(60, TimeUnit.SECONDS));
            SearchRequest searchRequest = new SearchRequest();
            searchRequest.indices(judgementIndex);
            searchRequest.source(sourceBuilder);
            SearchResponse searchResponse = null;

            try {
                searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
            } catch (IOException e) {
                e.printStackTrace();
            }

            SearchHits hits = searchResponse.getHits();
            float maxScore = hits.getMaxScore();
            resBuilder.put(entry.getKey(), maxScore);
            logger.info("query label: {}, words: {}, maxScore: {}", entry.getKey(), entry.getValue(), maxScore);
        }
        return resBuilder.build();
    }



}
